﻿// Chat form, (c)2010 Knihovnik
// This file is licensed under LGPL license, see more at http://www.gnu.org/licenses/lgpl.html
namespace FacebookChat
{

	#region Usings

	using System;
	using System.Collections.Generic;
	using System.Diagnostics;
	using System.Drawing;
	using System.IO;
	using System.Runtime.InteropServices;
	using System.Text;
	using System.Text.RegularExpressions;
	using System.Windows.Forms;
	using System.Xml;
	using jabber.client;
	using jabber.protocol.client;
	using jabber.protocol.iq;
	

	#endregion

	/// <summary>
	/// Chat window
	/// </summary>
	public partial class ChatForm : Form
	{

		#region Members

		private PresenceType m_presence;
		private bool m_isActive;
		private JabberClient m_client;
		private Item m_to;
		private string m_initialMessage;
		private StringBuilder m_html; // For future purposes (history,export,etc.)

		private static string _externalCss;
		private static Regex _linkRegex;
		private static List<Smiley> _smileys;

#if WINDOWS
		[DllImport("winmm.dll", SetLastError = true)]
		private static extern bool PlaySound(string pszSound, UIntPtr hmod, uint fdwSound);
#endif

		#endregion

		#region Constructors

		/// <summary>
		/// Static constructor
		/// </summary>
		static ChatForm()
		{
			_linkRegex = new Regex("http://([\\w+?\\.\\w+])+([a-zA-Z0-9\\~\\!\\@\\#\\$\\%\\^\\&amp;\\*\\(\\)_\\-\\=\\+\\\\\\/\\?\\.\\:\\;\\'\\,]*)?", RegexOptions.IgnoreCase);

			// Check for external css
			string path = Path.Combine(Application.StartupPath, "style.css");
			if (File.Exists(path))
				using (StreamReader reader = (File.OpenText(path)))
				{
					_externalCss = reader.ReadToEnd();
				}
			else _externalCss = null;

			// Initialize smileys
			_smileys = new List<Smiley>();
			_smileys.Add(new Smiley("Angel", new string[] { "O:-)", "O:)" }));
			_smileys.Add(new Smiley("Sad", new string[] { ":(", ":-(" }));
			_smileys.Add(new Smiley("Grin", new string[] { ":D", ":-D" }));
			_smileys.Add(new Smiley("Angry", new string[] { ">:o", ">:O" }));
			_smileys.Add(new Smiley("Crying", new string[] { ":'(" }));
			_smileys.Add(new Smiley("Kiss", new string[] { ":-*", ":-{}" }));
			_smileys.Add(new Smiley("Shock", new string[] { ":-o", ":-O" }));
			_smileys.Add(new Smiley("Sick", new string[] { ":-!" }));
			_smileys.Add(new Smiley("Thinking", new string[] { ":-/" }));
			_smileys.Add(new Smiley("Wink", new string[] { ";-)", ";)" }));
			_smileys.Add(new Smiley("Smile", new string[] { ":)", ":-)" }));
			_smileys.Add(new Smiley("Razz", new string[] { ":P", ":-P" }));
			
			_smileys.ForEach(delegate(Smiley smiley) { smiley.Extract(); });
		}

		/// <summary>
		/// Constructor with initial message
		/// </summary>
		/// <param name="to">Roster item (received from)</param>
		/// <param name="client">Instance of <see cref="JabberClient"/></param>
		/// <param name="message">Initial message</param>
		public ChatForm(Item to, JabberClient client, string message)
			: this(to, client)
		{
			this.m_initialMessage = message;
		}

		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="to">Roster item (received from)</param>
		/// <param name="client">Instance of <see cref="JabberClient"/></param>
		public ChatForm(Item to, JabberClient client)
		{
			this.InitializeComponent(); // Init UI

			this.wbExplorer.Navigated += delegate 
			{
				this.wbExplorer.Document.Write("<style>");

				// Initialize style
				if(!string.IsNullOrEmpty(_externalCss))
				{
					// Check for external style
					this.wbExplorer.Document.Write(_externalCss);
				}
				else
				{
					// Default style
					this.wbExplorer.Document.Write(string.Format("body {{margin:0px;margin-left:2px;padding:0px;font-family:{0};font-size:{1}pt}} ",
						this.Font.Name, this.Font.SizeInPoints));
					this.wbExplorer.Document.Write(".time { font-family:Courier New;font-size:10px} ");
					this.wbExplorer.Document.Write(".my {color:red;font-weight:bold} ");
					this.wbExplorer.Document.Write(".friend {color:#0094FF;font-weight:bold} ");
					this.wbExplorer.Document.Write(".status {color:grey}");
					this.wbExplorer.Document.Write("a {color:blue}");
					this.wbExplorer.Document.Write("a:visited {color:blue}");
				}

				this.wbExplorer.Document.Write("</style>");
				this.wbExplorer.NewWindow += new System.ComponentModel.CancelEventHandler(Explorer_NewWindow);

				if (!string.IsNullOrEmpty(this.m_initialMessage))
				{
					this.Append(this.m_to.Nickname, this.m_initialMessage, false);
					ChatForm.Pop();
				}
			};
			this.wbExplorer.Navigate("about:blank");
			
			// Init resource
			this.btSend.Text = Properties.Resources.Send;

			// Assigns
			this.m_client = client;
			this.m_to = to;
			this.m_client.OnMessage += new MessageHandler(this.Client_OnMessage);
			this.m_client.OnIQ += new IQHandler(this.Client_OnIQ);
			this.m_client.OnPresence += new PresenceHandler(this.Client_OnPresence);
			this.Text = to.Nickname;
			this.m_html = new StringBuilder();
			this.m_presence = PresenceType.available;

			// Request for contacts vCard
			VCardIQ vcard = new VCardIQ(this.m_client.Document);
			vcard.To = this.m_to.JID;
			vcard.Type = jabber.protocol.client.IQType.get;
			this.m_client.Write(vcard);
		}

		#endregion

		#region Properties

		/// <summary>
		/// Gets wheter is this form active or not
		/// </summary>
		public bool IsActive
		{
			get
			{
				return this.m_isActive;
			}
		}

		#endregion

		#region Override Methods

		protected override void OnActivated(EventArgs e)
		{
			this.m_isActive = true;
			base.OnActivated(e);
		}

		protected override void OnDeactivate(EventArgs e)
		{
			this.m_isActive = false;
			base.OnDeactivate(e);
		}

		protected override void OnClosed(EventArgs e)
		{
			// Unregister events
			this.m_client.OnMessage -= new MessageHandler(this.Client_OnMessage);
			this.m_client.OnIQ -= new IQHandler(this.Client_OnIQ);
			this.m_client.OnPresence -= new PresenceHandler(this.Client_OnPresence);
			this.wbExplorer.NewWindow -= new System.ComponentModel.CancelEventHandler(Explorer_NewWindow);
			if (this.Closed != null)
				this.Closed(this, EventArgs.Empty);
			base.OnClosed(e);
		}

		protected override void OnShown(EventArgs e)
		{
			base.OnShown(e);
			this.tbMessage.Focus();
		}

		#endregion

		#region Methods

		/// <summary>
		/// Sends users message
		/// </summary>
		private void Send()
		{
			if (this.tbMessage.Text.Trim().Length == 0)
				return;
			string message = this.tbMessage.Text;
			while (message.StartsWith(Environment.NewLine))
				message = message.Replace(Environment.NewLine, string.Empty);
			this.tbMessage.Text = string.Empty;
			this.m_client.Message(jabber.protocol.client.MessageType.chat, this.m_to.JID, message);
			this.Append(this.m_client.JID.User, message, true);
		}


		/// <summary>
		/// Plays pop.wav! :)
		/// </summary>
		internal static void Pop()
		{
#if WINDOWS
			string wave = Path.Combine(Application.StartupPath, "pop.wav");
			PlaySound(wave, UIntPtr.Zero, 0x0001 | 0x00020000);
#endif
		}

		/// <summary>
		/// Appends new message to the chat text field
		/// </summary>
		/// <param name="nickname">Nickname</param>
		/// <param name="text">Message text</param>
		/// <param name="self">Indicates wheter is this my message or not</param>
		private void Append(string nickname, string text, bool self)
		{
			if (string.IsNullOrEmpty(text) || text.Trim().Length == 0)
				return;

			this.Invoke(new MethodInvoker(delegate
			{
				string message = string.Format("<span class=\"time\">{0}</span> <span class=\"{3}\">{1}</span>: {2}<br/>",
					DateTime.Now.ToShortTimeString(), 
					nickname, 
					ChatForm.FormatMessage(text), 
					self ? "my" : "friend");
				
				this.m_html.Append(message);
				this.wbExplorer.Document.Write(message);
				this.wbExplorer.Document.Window.ScrollTo(0,
					this.wbExplorer.Document.Body.ScrollRectangle.Height);
			}));
		}

		/// <summary>
		/// Appends status message
		/// </summary>
		/// <param name="status">Status message</param>
		private void AppendStatus(string status)
		{
			this.Invoke(new MethodInvoker(delegate
			{
				string message = string.Format("<span class=\"time\">{0}</span> <span class=\"status\">{1}</span><br/>",
					DateTime.Now.ToShortTimeString(), status);
				this.m_html.Append(message);
				this.wbExplorer.Document.Write(message);
				this.wbExplorer.Document.Window.ScrollTo(0,
				this.wbExplorer.Document.Body.ScrollRectangle.Height);
			}));	
		}

		/// <summary>
		/// Formats message (links, smileys, etc.)
		/// </summary>
		/// <param name="message">Original unformatted message</param>
		/// <returns>Formatted message</returns>
		private static string FormatMessage(string message)
		{
			MatchCollection matches = _linkRegex.Matches(message);
			for(int i = matches.Count - 1; i > -1; --i)
			{
				Match match = matches[i];
				message = message.Remove(match.Index, match.Length);
				message = message.Insert(match.Index, string.Format("<a target=\"_blank\" href=\"{0}\">{0}</a>", match.Value));
			}

			for(int i = 0; i < _smileys.Count; ++i)
				message = _smileys[i].Replace(message);

			return message;
		}


		#endregion 

		#region Events

		/// <summary>
		/// Occurs when form is closed
		/// </summary>
		public new event EventHandler Closed;

		#endregion

		#region Event Handlers

		void Explorer_NewWindow(object sender, System.ComponentModel.CancelEventArgs e)
		{
			try
			{
				Process.Start(this.wbExplorer.StatusText);
			}
			catch (Exception ex)
			{
				Debug.Write(ex);
			}
			e.Cancel = true;
		}

		void Client_OnPresence(object sender, jabber.protocol.client.Presence pres)
		{
			if (pres.From.Bare == this.m_to.JID.Bare)
			{
				if (pres.Type == PresenceType.available && pres.Type != this.m_presence)
					this.AppendStatus(string.Format(Properties.Resources.UserOnline, this.m_to.Nickname));
				if (pres.Type == PresenceType.unavailable && pres.Type != this.m_presence)
					this.AppendStatus(string.Format(Properties.Resources.UserOffline, this.m_to.Nickname));
				this.m_presence = pres.Type;
			}
		}

		void Client_OnIQ(object sender, jabber.protocol.client.IQ iq)
		{
			XmlElement query = iq.Query;

			if (iq.From.Bare == this.m_to.JID.Bare)
			{
				if (query is VCard) // Get vCard
				{
					try
					{
						iq = iq.GetResponse(this.m_client.Document);
						VCard vcard = iq.Query as VCard;
						XmlElement el = vcard.Photo.GetFirstChildElement();
						string stringData = el.NextSibling.InnerText;
						MemoryStream stream = new MemoryStream(Convert.FromBase64String(stringData));
						Image avatar = Image.FromStream(stream);

						float d = 52 / (float)avatar.Width; // 52 is maximum Width. Why? 'cos I said so!!
						Bitmap resize = new Bitmap(avatar, new Size(52, (int) (avatar.Height * d)));
						this.Invoke(new MethodInvoker(delegate
							{
								this.pbAvatar.Image = resize;
								this.scContainer.Panel2MinSize = resize.Height;
								this.scContainer.SplitterDistance = this.scContainer.Height - resize.Height;
							}));
					}
					catch
					{
						this.Invoke(new MethodInvoker(delegate { this.pbAvatar.Visible = false; }));
					}
				}
			}
		}

		void Client_OnMessage(object sender, jabber.protocol.client.Message msg)
		{
			if (msg.From.Bare == this.m_to.JID.Bare)
			{
				this.Append(this.m_to.Nickname, msg.Body, false);
			}
		}

		private void tbMessage_TextChanged(object sender, EventArgs e)
		{
			this.btSend.Enabled = this.tbMessage.Text.Trim().Length > 0;
		}

		private void btSend_Click(object sender, EventArgs e)
		{
			this.Send();
		}

		private void tbMessage_KeyUp(object sender, KeyEventArgs e)
		{
			if (e.KeyCode == Keys.Enter)
			{
				e.SuppressKeyPress = true;
				e.Handled = true;
				this.Send();
			}
		}

		#endregion

	}
}
